﻿using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.Maui.SourceGenerators.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Maui.SourceGenerators.Internal;

[Generator]
public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator
{
	public const string BindablePropertyAttributeExperimentalDiagnosticId = "MCTEXP001";

	static readonly SemanticValues emptySemanticValues = new(default, []);

	const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty";
	const string bpAttribute =
		/* language=C#-test */
		//lang=csharp
		$$"""
		  // <auto-generated>
		  // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator

		  #pragma warning disable
		  #nullable enable
		  namespace CommunityToolkit.Maui;

		  [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
		  [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
		  [global::System.Diagnostics.CodeAnalysis.Experimental("{{BindablePropertyAttributeExperimentalDiagnosticId}}")]
		  sealed partial class BindablePropertyAttribute : global::System.Attribute
		  {
		  	public string? PropertyName { get; }
		  	public global::System.Type? DeclaringType { get; set; }
		  	public object? DefaultValue { get; set; }
		  	public global::Microsoft.Maui.Controls.BindingMode DefaultBindingMode { get; set; }
		  	public string ValidateValueMethodName { get; set; } = string.Empty;
		  	public string PropertyChangedMethodName { get; set; } = string.Empty;
		  	public string PropertyChangingMethodName { get; set; } = string.Empty;
		  	public string CoerceValueMethodName { get; set; } = string.Empty;
		  	public string DefaultValueCreatorMethodName { get; set; } = string.Empty;
		  }
		  """;

	public void Initialize(IncrementalGeneratorInitializationContext context)
	{
#if DEBUG

		if (!Debugger.IsAttached)
		{
			// To debug this SG, uncomment the line below and rebuild the SourceGenerator project.

			//Debugger.Launch();
		}
#endif

		context.RegisterPostInitializationOutput(static ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8)));

		var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute",
				IsNonEmptyPropertyDeclarationSyntax, SemanticTransform)
			.Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty)
			.Collect();


		context.RegisterSourceOutput(provider, ExecuteAllValues);
	}

	static void ExecuteAllValues(SourceProductionContext context, ImmutableArray<SemanticValues> semanticValues)
	{
		// Pre-allocate dictionary with expected capacity
		var groupedValues = new Dictionary<(string, string, string, string), List<SemanticValues>>(semanticValues.Length);

		// Single-pass grouping without LINQ
		foreach (var sv in semanticValues)
		{
			var key = (sv.ClassInformation.ClassName, sv.ClassInformation.ContainingNamespace, sv.ClassInformation.ContainingTypes, sv.ClassInformation.GenericTypeParameters);

			if (!groupedValues.TryGetValue(key, out var list))
			{
				list = [];
				groupedValues[key] = list;
			}
			list.Add(sv);
		}

		// Use ArrayPool for temporary storage
		var bindablePropertiesBuffer = System.Buffers.ArrayPool<BindablePropertyModel>.Shared.Rent(32);

		try
		{
			foreach (var keyValuePair in groupedValues)
			{
				var (className, containingNamespace, containingTypes, genericTypeParameters) = keyValuePair.Key;
				var values = keyValuePair.Value;

				if (values.Count is 0 || string.IsNullOrEmpty(className) || string.IsNullOrEmpty(containingNamespace))
				{
					continue;
				}

				// Flatten bindable properties without SelectMany allocation
				var bindablePropertiesCount = 0;
				foreach (var value in values)
				{
					foreach (var bp in value.BindableProperties)
					{
						if (bindablePropertiesCount >= bindablePropertiesBuffer.Length)
						{
							var newBuffer = System.Buffers.ArrayPool<BindablePropertyModel>.Shared.Rent(bindablePropertiesBuffer.Length * 2);
							Array.Copy(bindablePropertiesBuffer, newBuffer, bindablePropertiesBuffer.Length);
							System.Buffers.ArrayPool<BindablePropertyModel>.Shared.Return(bindablePropertiesBuffer);
							bindablePropertiesBuffer = newBuffer;
						}
						bindablePropertiesBuffer[bindablePropertiesCount++] = bp;
					}
				}

				var bindableProperties = ImmutableArray.Create(bindablePropertiesBuffer, 0, bindablePropertiesCount);

				var classAccessibility = values[0].ClassInformation.DeclaredAccessibility;

				var combinedClassInfo = new ClassInformation(className, classAccessibility, containingNamespace, containingTypes, genericTypeParameters);
				var combinedValues = new SemanticValues(combinedClassInfo, bindableProperties);

				var fileNameSuffix = string.IsNullOrEmpty(containingTypes) ? className : string.Concat(containingTypes, ".", className);
				var source = GenerateSource(combinedValues);
				SourceStringService.FormatText(ref source);
				context.AddSource($"{fileNameSuffix}.g.cs", SourceText.From(source, Encoding.UTF8));
			}
		}
		finally
		{
			System.Buffers.ArrayPool<BindablePropertyModel>.Shared.Return(bindablePropertiesBuffer);
		}
	}


	static string GenerateSource(SemanticValues value)
	{
		// Pre-calculate StringBuilder capacity to avoid resizing
		var estimatedCapacity = 500 + (value.BindableProperties.Count() * 400);
		var sb = new StringBuilder(estimatedCapacity);

		// Use string concatenation for simple cases
		sb.Append("// <auto-generated>\n// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator\n\n#pragma warning disable\n#nullable enable\n\n");

		if (!IsGlobalNamespace(value.ClassInformation))
		{
			sb.Append("namespace ").Append(value.ClassInformation.ContainingNamespace).Append(";\n\n");
		}

		// Generate nested class hierarchy
		if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes))
		{
			var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.');
			foreach (var typeName in containingTypeNames)
			{
				sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(typeName).Append("\n{\n\n");
			}
		}

		// Get the class name with generic parameters
		var classNameWithGenerics = value.ClassInformation.ClassName;
		if (!string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters))
		{
			classNameWithGenerics = string.Concat(value.ClassInformation.ClassName, "<", value.ClassInformation.GenericTypeParameters, ">");
		}

		sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(classNameWithGenerics).Append("\n{\n\n");

		foreach (var info in value.BindableProperties)
		{
			if (info.IsReadOnlyBindableProperty)
			{
				GenerateReadOnlyBindableProperty(sb, in info);
			}
			else
			{
				GenerateBindableProperty(sb, in info);
			}

			GenerateProperty(sb, in info);
		}

		sb.Append('}');

		// Close nested class hierarchy
		if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes))
		{
			var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.');
			for (int i = 0; i < containingTypeNames.Length; i++)
			{
				sb.Append("\n}");
			}
		}

		return sb.ToString();
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info)
	{
		// Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator
		var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType);
		var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;

		sb.Append("/// <summary>\r\n/// Backing BindableProperty for the <see cref=\"")
			.Append(sanitizedPropertyName)
			.Append("\"/> property.\r\n/// </summary>\r\n");

		// Generate BindablePropertyKey for read-only properties
		sb.Append("static readonly global::Microsoft.Maui.Controls.BindablePropertyKey ")
			.Append(info.BindablePropertyKeyName)
			.Append(" = \n")
			.Append(bpFullName)
			.Append(".CreateReadOnly(\"")
			.Append(sanitizedPropertyName)
			.Append("\", typeof(")
			.Append(GetFormattedReturnType(nonNullableReturnType))
			.Append("), typeof(")
			.Append(info.DeclaringType)
			.Append("), ")
			.Append(info.DefaultValue)
			.Append(", ")
			.Append(info.DefaultBindingMode)
			.Append(", ")
			.Append(info.ValidateValueMethodName)
			.Append(", ")
			.Append(info.PropertyChangedMethodName)
			.Append(", ")
			.Append(info.PropertyChangingMethodName)
			.Append(", ")
			.Append(info.CoerceValueMethodName)
			.Append(", ")
			.Append(info.DefaultValueCreatorMethodName)
			.Append(");\n");

		// Generate public BindableProperty from the key
		sb.Append("public ")
			.Append(info.NewKeywordText)
			.Append("static readonly ")
			.Append(bpFullName)
			.Append(' ')
			.Append(info.BindablePropertyName)
			.Append(" = ")
			.Append(info.BindablePropertyKeyName)
			.Append(".BindableProperty;\n");

		sb.Append('\n');
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info)
	{
		// Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator
		var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType);
		var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;

		sb.Append("/// <summary>\r\n/// Backing BindableProperty for the <see cref=\"")
			.Append(sanitizedPropertyName)
			.Append("\"/> property.\r\n/// </summary>\r\n");

		// Generate regular BindableProperty
		sb.Append("public ")
			.Append(info.NewKeywordText)
			.Append("static readonly ")
			.Append(bpFullName)
			.Append(' ')
			.Append(info.BindablePropertyName)
			.Append(" = \n")
			.Append(bpFullName)
			.Append(".Create(\"")
			.Append(sanitizedPropertyName)
			.Append("\", typeof(")
			.Append(GetFormattedReturnType(nonNullableReturnType))
			.Append("), typeof(")
			.Append(info.DeclaringType)
			.Append("), ")
			.Append(info.DefaultValue)
			.Append(", ")
			.Append(info.DefaultBindingMode)
			.Append(", ")
			.Append(info.ValidateValueMethodName)
			.Append(", ")
			.Append(info.PropertyChangedMethodName)
			.Append(", ")
			.Append(info.PropertyChangingMethodName)
			.Append(", ")
			.Append(info.CoerceValueMethodName)
			.Append(", ")
			.Append(info.DefaultValueCreatorMethodName)
			.Append(");\n");

		sb.Append('\n');
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
	{
		var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;
		var formattedReturnType = GetFormattedReturnType(info.ReturnType);

		sb.Append("public ")
			.Append(info.NewKeywordText)
			.Append("partial ")
			.Append(formattedReturnType)
			.Append(' ')
			.Append(sanitizedPropertyName)
			.Append("\n{\nget => (")
			.Append(formattedReturnType)
			.Append(")GetValue(")
			.Append(info.BindablePropertyName)
			.Append(");\n");

		if (info.SetterAccessibility is not null)
		{
			sb.Append(info.SetterAccessibility)
				.Append("set => SetValue(")
				.Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName)
				.Append(", value);\n");
		}
		// else Do not create a Setter because the property is read-only

		sb.Append("}\n");
	}

	static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
	{
		var propertyDeclarationSyntax = Unsafe.As<PropertyDeclarationSyntax>(context.TargetNode);
		var semanticModel = context.SemanticModel;
		var propertySymbol = (IPropertySymbol?)ModelExtensions.GetDeclaredSymbol(semanticModel, propertyDeclarationSyntax, cancellationToken);

		if (propertySymbol is null)
		{
			return emptySemanticValues;
		}

		var @namespace = propertySymbol.ContainingNamespace.ToDisplayString();
		var className = propertySymbol.ContainingType.Name;
		var classAccessibility = propertySymbol.ContainingSymbol.DeclaredAccessibility.ToString().ToLower();
		var returnType = propertySymbol.Type;

		// Build containing types hierarchy
		var containingTypes = GetContainingTypes(propertySymbol.ContainingType);

		// Extract generic type parameters
		var genericTypeParameters = GetGenericTypeParameters(propertySymbol.ContainingType);

		var propertyInfo = new ClassInformation(className, classAccessibility, @namespace, containingTypes, genericTypeParameters);

		// Use array instead of List to avoid resizing
		var bindablePropertyModels = new BindablePropertyModel[context.Attributes.Length];

		var doesContainNewKeyword = HasNewKeyword(propertyDeclarationSyntax);
		var (isReadOnlyBindableProperty, setterAccessibility) = GetPropertyAccessibility(propertySymbol, propertyDeclarationSyntax);

		var attributeData = context.Attributes[0];
		bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility);

		return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels));
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static bool HasNewKeyword(PropertyDeclarationSyntax syntax)
	{
		foreach (var modifier in syntax.Modifiers)
		{
			if (modifier.IsKind(SyntaxKind.NewKeyword))
			{
				return true;
			}
		}
		return false;
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static (bool IsReadOnlyBindableProperty, string? SetterAccessibility) GetPropertyAccessibility(IPropertySymbol propertySymbol, PropertyDeclarationSyntax syntax)
	{
		// Check if property is get-only (no setter)
		if (propertySymbol.SetMethod is null)
		{
			return (true, null);
		}

		return propertySymbol.SetMethod.DeclaredAccessibility switch
		{
			Accessibility.NotApplicable => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"),
			Accessibility.Private => (true, "private "),
			Accessibility.ProtectedAndInternal => (true, "private protected "),
			Accessibility.Protected => (true, "protected "),
			Accessibility.Internal => (false, "internal "),
			Accessibility.ProtectedOrInternal => (false, "protected internal "),
			Accessibility.Public => (false, " "), // Keep the SetterAccessibility empty because the Property is public and the setter will inherit that accessbility modified, e.g. `public string Test { get; set; }`
			_ => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"),
		};
	}

	static string GetContainingTypes(INamedTypeSymbol typeSymbol)
	{
		var current = typeSymbol.ContainingType;
		if (current is null)
		{
			return string.Empty;
		}

		var sb = new StringBuilder(100);
		var stack = new Stack<string>(4);

		while (current is not null)
		{
			stack.Push(current.Name);
			current = current.ContainingType;
		}

		var first = true;
		while (stack.Count > 0)
		{
			if (!first)
			{
				sb.Append('.');
			}
			sb.Append(stack.Pop());
			first = false;
		}

		return sb.ToString();
	}

	static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol)
	{
		if (!typeSymbol.IsGenericType || typeSymbol.TypeParameters.IsEmpty)
		{
			return string.Empty;
		}

		var typeParams = typeSymbol.TypeParameters;
		if (typeParams.Length == 1)
		{
			return typeParams[0].Name;
		}

		var sb = new StringBuilder(typeParams.Length * 10);
		for (int i = 0; i < typeParams.Length; i++)
		{
			if (i > 0)
			{
				sb.Append(", ");
			}
			sb.Append(typeParams[i].Name);
		}

		return sb.ToString();
	}

	static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility)
	{
		if (attributeData.AttributeClass is null)
		{
			throw new ArgumentException($"{nameof(attributeData)}.{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData));
		}

		var defaultValue = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultValue), returnType);
		var coerceValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName));
		var defaultBindingMode = attributeData.GetNamedTypeArgumentsAttributeValueForDefaultBindingMode(nameof(BindablePropertyModel.DefaultBindingMode), "Microsoft.Maui.Controls.BindingMode.OneWay");
		var defaultValueCreatorMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName));
		var propertyChangedMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName));
		var propertyChangingMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName));
		var validateValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));
		var newKeywordText = doesContainNewKeyword ? "new " : string.Empty;

		return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility);
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static ITypeSymbol ConvertToNonNullableTypeSymbol(in ITypeSymbol typeSymbol)
	{
		// Check for Nullable<T>
		if (typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T })
		{
			return typeSymbol;
		}

		// Check for Nullable Reference Type
		if (typeSymbol.NullableAnnotation is NullableAnnotation.Annotated)
		{
			// For reference types, NullableAnnotation.None indicates non-nullable.
			return typeSymbol.WithNullableAnnotation(NullableAnnotation.None);
		}

		return typeSymbol;
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static bool IsNonEmptyPropertyDeclarationSyntax(SyntaxNode node, CancellationToken cancellationToken)
	{
		cancellationToken.ThrowIfCancellationRequested();

		return node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 };
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static bool IsDotnetKeyword(in string name) => SyntaxFacts.GetKeywordKind(name) is not SyntaxKind.None;

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	static bool IsGlobalNamespace(in ClassInformation classInformation)
	{
		return classInformation.ContainingNamespace is "<global namespace>";
	}

	static string GetFormattedReturnType(ITypeSymbol typeSymbol)
	{
		if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol)
		{
			// Get the element type name (e.g., "int")
			string elementType = GetFormattedReturnType(arrayTypeSymbol.ElementType);

			// Construct the correct rank syntax with commas (e.g., "[,]")
			var rank = arrayTypeSymbol.Rank > 1 ? new string(',', arrayTypeSymbol.Rank - 1) : string.Empty;

			return string.Concat(elementType, "[", rank, "]");
		}
		else
		{
			// Use ToDisplayString with the correct format for the base type (e.g., "int")
			return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
		}
	}
}